// javamaps
// Copyright (c) 2010 Lup Gratian
package Rendering.Renderers;
import Core.*;
import Rendering.*;
import Rendering.Utils.*;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.util.ArrayList;
import java.awt.image.*;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import javax.swing.SwingUtilities;

public class ImageRenderer implements IRenderer {
    /*
     * Membrii.
     */
    IMapProvider provider_;
    IRendererHost host_;
    ILayer layer_;
    int zoomLevels_;
    double tileSize_;
    ArrayList<TileInfo> tiles_;
    ArrayList<TileInfo> nextTiles_;
    ImagePrefetcher prefetcher_;
    ImageCache cache_;
    double deltaLeft_;
    double deltaRight_;
    double deltaTop_;
    double deltaBottom_;
    double opacity_;
    double prevLeft_;
    double prevTop_;
    double prevRight_;
    double prevBottom_;
    double prevZoom_;

    /*
     * Constructori.
     */
    public ImageRenderer(ILayer layer, IRendererHost host) {
        host_ = host;
        layer_ = layer;
        provider_ = host.MapProvider();
        zoomLevels_ = provider_.ZoomLevels();

        // Creaza lista cu imagini de dimensiune maxima.
        tileSize_ = provider_.TileSize();
        tiles_ = new ArrayList<TileInfo>(provider_.TileCount(zoomLevels_ - 1));
        nextTiles_ = new ArrayList<TileInfo>(provider_.TileCount(zoomLevels_ - 1));

        // Initializeaza sistemul de cache.
        cache_ = new ImageCache(provider_);
        prefetcher_ = new ImagePrefetcher(this, provider_, cache_);
        prevZoom_ = -1.0;
    }

    /*
     * Metode publice.
     */
    public boolean IsEditor() { return false; }
    public ILayer Layer() { return layer_; }

    public int ZIndex() { return 0; } // Primul strat desenat.
    public void SetZIndex(int value) {
        throw new UnsupportedOperationException("ZIndex of image layer cannot be changed.");
    }
    
    public boolean HasPrefetcher() { return false; }
    public IPrefetcher Prefetcher() { return null; }

    public boolean MouseDown(Point point, View view, Modifier modifier) { return false; }
    public boolean MouseUp(Point point, View view, Modifier modifier) { return false; }
    public boolean MouseMoved(Point point, View view, Modifier modifier) { return false; }
    public boolean MouseDragged(Point point, View view, Modifier modifier) { return false; }
    public boolean MouseCaptured() { return false; } // Nu poate captura mouse-ul.
    public IObjectInfo HitTest(Point point, View view) { return null; }

    public double Opacity() { return 1.0; } // Intotdeauna opac.
    public void SetOpacity(double value) {}
    public boolean Visible() { return true; } // Nu poate fi ascuns.
    public void SetVisible(boolean value) {}

    public void Render(View view) {
        // Calculeaza deplasamentul fata de pozitia precedenta.
        // Imaginile din regiunea curenta sunt cerute de la provider
        // doar daca deplasamentul este mai mare decat dimensiunea unei
        // imagini sau s-a schimbat nivelul de zoom.
        double dLeft = view.Bounds().Left() - prevLeft_;
        double dTop = view.Bounds().Top() - prevTop_;
        double dRight = view.Bounds().Right() - prevRight_;
        double dBottom = view.Bounds().Bottom() - prevBottom_;

        // Incarca imaginile daca este necesar.
        QueryTiles(view, dLeft, dTop, dRight, dBottom);
        Draw(view);
        SetPreviousParams(view);
    }

    public void TileLoaded(Image image, TileInfo tile) {
        // Forteaza parintele sa se redeseneze
        // (notificarea se face pe firul de executie al interfetei).
        Runnable notify = new Runnable() {
            public void run() { host_.Repaint(); }
        };
        
        SwingUtilities.invokeLater(notify);
    }

    /*
     * Metode private.
     */
    // Salveaza parametrii curenti pentru a putea fi folositi
    // la urmatoarea redesenare a imaginilor.
    private void SetPreviousParams(View view) {
        Region2D bounds = view.Bounds();
        prevLeft_ = bounds.Left();
        prevTop_ = bounds.Top();
        prevRight_ = bounds.Right();
        prevBottom_ = bounds.Bottom();
        prevZoom_ = view.Zoom();
    }

    // Obtine imaginea asociata cu Id-ul specificat.
    private Image GetTile(TileInfo tile, boolean load) {
        // Verifica daca imaginea se afla in cache.
        Image tileImage = cache_.Get(tile.Id(), tile.ZoomLevel());
        if((tileImage == null) && load) {
            // Imaginea nu este inca in cache, fa o cerere
            // de incarcare asincrona la prefetcher.
            prefetcher_.LoadTile(tile);
        }

        return tileImage;
    }

    // Obtine lista cu imagini ce trebuie desenate pentru pozitia curenta.
    private void QueryTiles(View view, double dLeft, double dTop,
                                       double dRight, double dBottom) {
        // Cere informatiile de la provider doar daca este necesar.
        deltaLeft_ += dLeft;
        deltaRight_ -= dRight;
        deltaTop_ += dTop;
        deltaBottom_ -= dBottom;

        if((deltaLeft_ < 0) || (deltaRight_ < 0) ||
           (deltaTop_ < 0) || (deltaBottom_ < 0) ||
           ((int)view.Zoom() != (int)prevZoom_)) {
            // Cere imaginile din regiune.
            tiles_.clear();
            provider_.GetTiles(view.PreviousBounds(), (int)view.Zoom(), tiles_);

            // Pentru a calcula distantele fata de margini
            // este suficient sa verifica prima si ultima imagine.
            TileInfo first = tiles_.get(0);
            TileInfo last = tiles_.get(tiles_.size() - 1);
            Region2D bounds = view.Bounds();

            deltaLeft_ = bounds.Left() - first.X();
            deltaRight_ = (last.X() + tileSize_) - bounds.Right();
            deltaTop_ = bounds.Top() - first.Y();
            deltaBottom_ = (last.Y() + tileSize_) - bounds.Bottom();

            int nextZoom = (int)Math.ceil(view.Zoom());
            if((int)view.Zoom() != nextZoom) {
                // Obtine imaginile pentru urmatorul nivel de zoom.
                nextTiles_.clear();
                provider_.GetTiles(view.NextBounds(), nextZoom, nextTiles_);
            }
        }
    }

    // Afiseaza imaginile din lista specificata, scalate corespunzator.
    private void DrawImpl(ArrayList<TileInfo> tiles, Graphics g,
                          View view, double scale) {
        // Deseneaza toate imaginile din lista.
        double viewX = view.Bounds().Left();
        double viewY = view.Bounds().Top();

        int ct = 0;
        int count = tiles.size();
        for(int i = 0; i < count; i++) {
            TileInfo tile = tiles.get(i);
            double destLeft = (tile.X() * scale) - viewX;
            double destTop = (tile.Y() * scale) - viewY;
            double destRight = destLeft + (tileSize_ * scale);
            double destBottom = destTop + (tileSize_ * scale);

            // Obtine imaginea de la sistemul de cache.
            Image tileImage = GetTile(tile, true);
            if(tileImage == null) {
                // Imaginea nu a fost inca incarcata, obtine o imagine
                // la o rezolutie mai mica care va fi ulterior scalata.
                double size = tileSize_;
                double x = tile.X();
                double y = tile.Y();
                int prevZoom = tile.ZoomLevel();

                while(prevZoom > 0) {
                    // Ajusteaza punctul la nivelul curent de zoom.
                    x /= 2;
                    y /= 2;
                    size /= 2;
                    prevZoom--;

                    // Incearca obtinearea imaginii asociate (nu este
                    // incarcata daca nu se afla deja in cache).
                    tile = provider_.GetTile(new Core.Point(x, y), prevZoom);
                    tileImage = GetTile(tile, false);
                    if(tileImage != null) {
                        // Scaleaza imaginea corespunzator.
                        x = x - tile.X();
                        y = y - tile.Y();
                        g.drawImage(tileImage, (int)destLeft,
                                               (int)destTop,
                                               (int)destRight,
                                               (int)destBottom,
                                               (int)x, (int)y,
                                               (int)(x + size), (int)(y + size),
                                               (Component)host_);

                        break; // while(tile.ZoomLevel() > 0)
                    }
                }
            }
            else {
                // Imaginea la rezolutie corespunzatoare a fost incarcata.
                g.drawImage(tileImage,
                            (int)destLeft, (int)destTop,
                            (int)destRight,(int)destBottom,
                            0, 0, (int)tileSize_, (int)tileSize_,
                            (Component)host_);
            }
        }
    }

    // Afiseaza imaginile la nivelul de zoom curent.
    private void Draw(View view) {
        VolatileImage buffer = view.GetBuffer(this);
        Graphics2D g = buffer.createGraphics();
        g.clearRect(0, 0, (int)view.ViewBounds().Width(), (int)view.ViewBounds().Height());

        // Calculeaza deviatia de la valoarea intreaga a zoom-ului.
        double position = view.Zoom() - Math.floor(view.Zoom());
        if(position >= 0.3) {
            // Afiseaza imaginile pentru nivelul de zoom urmator.
            DrawImpl(nextTiles_, g, view, 0.5 + (position / 2));
        }

        // Afiseaza imaginile pentru nivelul de zoom curent.
        if(position >= 0.3) {
            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                           (float)(1.0 - position)));
        }
        else {
            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        }

        DrawImpl(tiles_, g, view, 1.0 + position);
        g.dispose();
    }
}